# Copyright 2012 Steven Watanabe
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE.txt or https://www.bfgroup.xyz/b2/LICENSE.txt)

import os ;
import targets ;
import project ;
import "class" : new ;
import virtual-target ;
import configure ;
import path ;
import property ;
import property-set ;
import common ;

rule get-root-project ( project )
{
    # Find the root project.
    local root-project = $(project) ;
    root-project = [ $(root-project).project-module ] ;
    while
        [ project.attribute $(root-project) parent-module ] &&
        [ project.attribute $(root-project) parent-module ] != user-config &&
        [ project.attribute $(root-project) parent-module ] != project-config
    {
        root-project = [ project.attribute $(root-project) parent-module ] ;
    }
    return $(root-project) ;
}

TOUCH = [ common.file-touch-command ] ;

actions touch {
   $(TOUCH) "$(<)"
}

rule can-symlink ( project )
{
    if ! $(.can-symlink)
    {
        local root-project = [ get-root-project $(project) ] ;

        local source-target = [ new file-target test-symlink-source : :
            $(project) : [ new action : link.touch ] ] ;
        local target = [ new file-target test-symlink : :
            $(project) : [ new action $(source-target) : link.mklink ] ] ;

        if [ configure.try-build $(target) : [ property-set.empty ] : "symlinks supported" ]
        {
            .can-symlink = true ;
        }
        else
        {
            .can-symlink = false ;
        }
    }
    if $(.can-symlink) = true
    {
        return true ;
    }
}

if [ os.name ] = NT
{

# Test for Windows junctions (mklink /J)
rule can-junction ( project )
{
    if ! $(.can-junction)
    {
        local root-project = [ get-root-project $(project) ] ;

        local source-target = [ new file-target test-junction-source : :
            $(project) : [ new action : common.mkdir ] ] ;
        local target = [ new file-target test-junction : :
            $(project) : [ new action $(source-target) : link.junction ] ] ;

        if [ configure.try-build $(target) : [ property-set.empty ] : "junctions supported" ]
        {
            .can-junction = true ;
        }
        else
        {
            .can-junction = false ;
        }
    }
    if $(.can-junction) = true
    {
        return true ;
    }
}

}
else
{

.can-junction = false ;

rule can-junction ( project )
{
}

}

rule can-hardlink ( project )
{
    if ! $(.can-hardlink)
    {
        local root-project = [ get-root-project $(project) ] ;

        local source-target = [ new file-target test-hardlink-source : :
            $(project) : [ new action : link.touch ] ] ;
        # Use <location-prefix> so that the destination link is created
        # in a different directory. AFS refuses to make hard links
        # between files in different directories, so we want to check
        # it.
        local target = [ new file-target test-hardlink : :
            $(project) : [ new action $(source-target) : link.hardlink
            : [ new property-set <location-prefix>symlink ]
            ] ] ;

        if [ configure.try-build $(target) : [ property-set.empty ] : "hardlinks supported" ]
        {
            .can-hardlink = true ;
        }
        else
        {
            .can-hardlink = false ;
        }
    }
    if $(.can-hardlink) = true
    {
        return true ;
    }
}

class file-or-directory-reference : basic-target
{
    import virtual-target ;
    import property-set ;
    import path ;

    rule construct ( name : source-targets * : property-set )
    {
        return [ property-set.empty ] [ virtual-target.from-file $(self.name) :
            [ location ] : $(self.project) ] ;
    }

    # Returns true if the referred file really exists.
    rule exists ( )
    {
        location ;
        return $(self.file-path) ;
    }

    # Returns the location of target. Needed by 'testing.jam'.
    rule location ( )
    {
        if ! $(self.file-location)
        {
            local source-location = [ $(self.project).get source-location ] ;
            for local src-dir in $(source-location)
            {
                if ! $(self.file-location)
                {
                    local location = [ path.root $(self.name) $(src-dir) ] ;
                    if [ path.exists [ path.native $(location) ] ]
                    {
                         self.file-location = $(src-dir) ;
                         self.file-path = $(location) ;
                    }
                }
            }
        }
        return $(self.file-location) ;
    }
}

class symlink-target-class : basic-target
{
    import path ;
    import virtual-target ;
    import link ;
    import os ;
    import type ;
    rule construct ( name : source-target : property-set )
    {
        local location = [ path.join
            [ $(source-target).path ] [ $(source-target).name ] ] ;
        local files = [ path.glob-tree $(location) : * ] ;
        local targets ;

        # If we have symlinks, don't bother checking
        # for hardlinks and junctions.
        if ! [ link.can-symlink $(self.project) ]
        {
            link.can-junction $(self.project) ;
            link.can-hardlink $(self.project) ;
        }

        if [ $(property-set).get <location> ]
        {
            property-set = [ property-set.create
                [ property.select <location> : [ $(property-set).raw ] ] ] ;
        }
        else
        {
            local path,relative-to-build-dir = [ $(property-set).target-path ] ;
            local path = $(path,relative-to-build-dir[1]) ;
            local relative-to-build-dir = $(path,relative-to-build-dir[2]) ;

            if $(relative-to-build-dir)
            {
                path = [ path.join [ $(self.project).build-dir ] $(path) ] ;
            }

            property-set = [ property-set.create <location>$(path) ] ;
        }

        local a = [ new non-scanning-action $(source-target) :
            link.do-link-recursively : $(property-set) ] ;

        local t = [ new notfile-target $(name)
            : $(self.project) : $(a) ] ;

        return [ property-set.empty ] [ virtual-target.register $(t) ] ;
    }
}

rule do-file-link
{
    local target = [ path.native [ path.relative-to [ path.pwd ] $(<) ] ] ;
    local source = [ path.native [ path.relative-to [ path.pwd ] $(>) ] ] ;
    local old-source = [ on $(target) return $(LINK-SOURCE) ] ;
    if $(old-source)
    {
        import errors ;
        errors.user-error
            Cannot create link $(target) to $(source). :
            Link previously defined to another file, $(old-source[1]). ;
    }
    LINK-SOURCE on $(target) = $(source) $(.current-target) ;
    LOCATE on $(target) = . ;
    DEPENDS $(.current-target) : $(target) ;
    if $(.can-symlink) = true
    {
        DEPENDS $(target) : $(source) ;
        link.mklink $(target) : $(source) ;
    }
    else if $(.can-hardlink) = true
    {
        DEPENDS $(target) : $(source) ;
        link.hardlink $(target) : $(source) ;
    }
    else
    {
        DEPENDS $(target) : $(source) ;
        common.copy $(target) : $(source) ;
    }
}

rule do-link
{
    local target = [ path.native [ path.relative-to [ path.pwd ] $(<) ] ] ;
    local source = [ path.native [ path.relative-to [ path.pwd ] $(>) ] ] ;
    local relative = [ path.native [ path.relative-to [ path.parent $(<) ] $(>) ] ] ;
    if ! [ on $(target) return $(MKLINK_OR_DIR) ]
    {
        LOCATE on $(target) = . ;
        DEPENDS $(.current-target) : $(target) ;
        mklink-or-dir $(target) : $(source) ;
    }
    if [ os.name ] = NT
    {
        if $(.can-symlink) = true
        {
            MKLINK_OR_DIR on $(target) = mklink /D \"$(target)\" \"$(relative)\" ;
        }
        else
        {
            # This function should only be called
            # if either symlinks or junctions are supported.
            # To get here $(.can-junction) must be true.
            mklink-opt = /J ;
            MKLINK_OR_DIR on $(target) = mklink /J \"$(target)\" \"$(source)\" ;
        }
    }
    else
    {
        MKLINK_OR_DIR on $(target) = ln -s $(relative) $(target)  ;
    }
}

rule force-update
{
    local target = [ path.native [ path.relative-to [ path.pwd ] $(<) ] ] ;
    ALWAYS $(target) ;
}

rule do-split
{
    local target = [ path.native [ path.relative-to [ path.pwd ] $(<) ] ] ;
    if ! [ on $(target) return $(MKLINK_OR_DIR) ]
    {
        LOCATE on $(target) = . ;
        DEPENDS $(.current-target) : $(target) ;
        common.mkdir $(target) ;
    }
    MKLINK_OR_DIR on $(target) = mkdir \"$(target)\" ;
}

rule do-rm
{
    local target = [ path.native [ path.relative-to [ path.pwd ] $(<) ] ] ;
    ALWAYS $(target) ;
    RM on $(target) = rmdir ;
    link.rm $(target) ;
}

rule mklink-or-dir
{
    NOUPDATE $(<) ;
}

actions mklink-or-dir
{
    $(MKLINK_OR_DIR)
}

rule link-entries ( target : files * : split ? : deleted ? )
{
    for local s in $(files)
    {
        local t = [ path.join $(target) [ path.basename $(s) ] ] ;
        if ! $(.known-dirs.$(t))
        {
            local t = [ path.native [ path.relative-to [ path.pwd ] $(t) ] ] ;
            local s = [ path.native [ path.relative-to [ path.pwd ] $(target) ] ] ;
            LOCATE on $(t) = . ;
            DEPENDS $(t) : $(s) ;
            NOUPDATE $(s) ;
        }
        if $(split)
        {
            link-recursively $(t) : $(s) : : $(deleted) ;
        }
        else
        {
            link-entries $(t) : [ path.glob $(s) : * ] ;
        }
    }
    if ! $(.known-dirs.$(target))
    {
        .known-dirs.$(target) += $(files) ;
        .known-dirs.base.$(target) = $(.current-target) ;
    }
}

rule link-recursively ( target : source : no-recurse ? : deleted ? )
{
    if $(deleted) {
        force-update $(target) ;
    }

    local split ;
    if [ CHECK_IF_FILE [ path.native $(source) ] ]
    {
        do-file-link $(target) : $(source) ;
    }
    else if $(.known-dirs.$(target)) && ! $(no-recurse)
    {
        split = true ;
        if ! $(.split-dirs.$(target))
        {
            if [ READLINK [ path.native $(target) ] ]
            {
                if ! $(deleted) {
                    do-rm $(target) ;
                    deleted = true ;
                    .deleted-dirs.$(target) = true ;
                }
            }
            local .current-target = $(.known-dirs.base.$(target)) ;
            for local s in $(.known-dirs.$(target))
            {
                local t = [ path.join $(target) [ path.basename $(s) ] ] ;
                link-recursively $(t) : $(s) : flat : $(deleted) ;
            }
            do-split $(target) ;
        }
        else if $(.deleted-dirs.$(target))
        {
            deleted = true ;
        }
    }
    else if [ path.exists [ path.native $(target) ] ] && ! $(deleted)
    {
        local link-target = [ READLINK [ path.native $(target) ] ] ;
        if $(link-target)
        {
            local full-path =
                [ path.root [ path.make $(link-target) ] [ path.parent $(target) ] ] ;
            # HACK: Take advantage of the fact that path.glob
            # normalizes its arguments.  If full-path and
            # source are different, but both are empty, they
            # will compare equal, but that's okay because
            # for the purposes of this module, empty directories
            # are equivalent.
            if [ path.glob $(full-path) : * ] != [ path.glob $(source) : * ]
            {
                if ! $(deleted) {
                    do-rm $(target) ;
                    deleted = true ;
                    .deleted-dirs.$(target) = true ;
                }
                do-split $(target) ;
                split = true ;
            }
        }
        else
        {
            do-split $(target) ;
            split = true ;
        }
    }
    else if $(.can-symlink) = false && $(.can-junction) = false
    {
        if [ READLINK [ path.native $(target) ] ]
        {
            if ! $(deleted) {
                do-rm $(target) ;
                deleted = true ;
                .deleted-dirs.$(target) = true ;
            }
        }
        do-split $(target) ;
        split = true ;
    }
    else
    {
        do-link $(target) : $(source) ;
    }

    if $(split)
    {
        .split-dirs.$(target) = true ;
    }

    if ! $(no-recurse)
    {
        link-entries $(target) : [ path.glob $(source) : * ] : $(split) : $(deleted) ;
    }
}

rule do-link-recursively ( target : source : properties * )
{
    local target-path = [ property.select <location> : $(properties) ] ;
    local source-path = [ on $(source) return $(LOCATE) ] [ on $(source) return $(SEARCH) ] ;

    local absolute-target = [ path.root
        [ path.join [ path.make $(target-path[1]:G=) ]
                    [ path.basename [ path.make $(source:G=) ] ] ]
        [ path.pwd ] ] ;

    local absolute-source = [ path.root
        [ path.root [ path.make $(source:G=) ]
                    [ path.make $(source-path[1]) ] ]
        [ path.pwd ] ] ;

    local .current-target = $(target) ;

    link-recursively $(absolute-target) : $(absolute-source) ;
}

rule mklink
{
    local target-path = [ on $(<) return $(LOCATE) ] [ on $(<) return $(SEARCH) ] . ;
    local source-path = [ on $(>) return $(LOCATE) ] [ on $(>) return $(SEARCH) ] . ;
    local relative-path = [ path.relative-to
        [ path.parent [ path.join [ path.root [ path.make $(target-path[1]) ] [ path.pwd ] ] [ path.make $(<:G=) ] ] ]
        [ path.join [ path.root [ path.make $(source-path[1]) ] [ path.pwd ] ] [ path.make $(>:G=) ] ] ] ;

    PATH_TO_SOURCE on $(<) = [ path.native $(relative-path) ] ;
}

if [ os.name ] = NT
{

actions junction
{
    if exist "$(<)" rmdir "$(<)"
    mklink /J "$(<)" "$(>)"
}

actions mklink
{
    if exist "$(<)" del "$(<)"
    mklink "$(<)" "$(PATH_TO_SOURCE)"
}

actions hardlink
{
    if exist "$(<)" del "$(<)"
    mklink /H "$(<)" "$(>)"
}

actions rm
{
    rmdir "$(<)"
}

}
else
{

actions mklink
{
    ln -f -s "$(PATH_TO_SOURCE)" "$(<)"
}

actions hardlink
{
    ln -f "$(>)" "$(<)"
}

actions rm
{
    rm "$(<)"
}

}

rule link-directory ( name : sources : requirements * : default-build * : usage-requirements * )
{
    local project = [ project.current ] ;
    sources = [ new file-or-directory-reference $(sources) : $(project) ] ;
    targets.main-target-alternative $(sources) ;
    return [ targets.main-target-alternative
        [ new symlink-target-class $(name) : $(project)
            : [ targets.main-target-sources $(sources) : $(name) : no-renaming ]
            : [ targets.main-target-requirements $(requirements) : $(project) ]
            : [ targets.main-target-default-build : $(project) ]
            : [ targets.main-target-usage-requirements $(usage-requirements) :
                $(project) ] ] ] ;
}

IMPORT $(__name__) : link-directory : : link-directory ;
